<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>实时语音识别 - WebSocket 流式传输</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 20px;
        }

        .container {
            background: white;
            border-radius: 20px;
            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
            padding: 40px;
            max-width: 800px;
            width: 100%;
        }

        h1 {
            color: #333;
            margin-bottom: 10px;
            text-align: center;
        }

        .subtitle {
            color: #666;
            text-align: center;
            margin-bottom: 30px;
            font-size: 14px;
        }

        /* 连接状态指示器 */
        .connection-status {
            display: flex;
            align-items: center;
            justify-content: center;
            margin-bottom: 20px;
            padding: 10px;
            border-radius: 10px;
            background: #f0f0f0;
        }

        .status-indicator {
            width: 10px;
            height: 10px;
            border-radius: 50%;
            margin-right: 10px;
            animation: pulse 2s infinite;
        }

        .status-indicator.connected {
            background: #48bb78;
        }

        .status-indicator.disconnected {
            background: #f56565;
            animation: none;
        }

        .status-indicator.connecting {
            background: #f6ad55;
        }

        @keyframes pulse {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.5; }
        }

        /* 控制按钮 */
        .controls {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
            gap: 15px;
            margin-bottom: 30px;
        }

        button {
            padding: 15px;
            border: none;
            border-radius: 10px;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
        }

        button:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }

        .btn-primary {
            background: #667eea;
            color: white;
        }

        .btn-primary:hover:not(:disabled) {
            background: #5568d3;
            transform: translateY(-2px);
        }

        .btn-success {
            background: #48bb78;
            color: white;
        }

        .btn-success:hover:not(:disabled) {
            background: #38a169;
            transform: translateY(-2px);
        }

        .btn-danger {
            background: #f56565;
            color: white;
        }

        .btn-danger:hover:not(:disabled) {
            background: #e53e3e;
            transform: translateY(-2px);
        }

        .btn-recording {
            background: #f56565;
            animation: recording-pulse 1.5s infinite;
        }

        @keyframes recording-pulse {
            0%, 100% { 
                box-shadow: 0 0 0 0 rgba(245, 101, 101, 0.4);
            }
            50% { 
                box-shadow: 0 0 0 10px rgba(245, 101, 101, 0);
            }
        }

        /* 实时转录区域 */
        .transcription-area {
            background: #f7fafc;
            border: 2px solid #e2e8f0;
            border-radius: 10px;
            padding: 20px;
            min-height: 200px;
            margin-bottom: 20px;
        }

        .transcription-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
        }

        .transcription-title {
            color: #2d3748;
            font-size: 18px;
            font-weight: 600;
        }

        .clear-btn {
            padding: 5px 10px;
            background: #e2e8f0;
            color: #4a5568;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 12px;
        }

        .clear-btn:hover {
            background: #cbd5e0;
        }

        /* 实时文本显示 */
        .realtime-text {
            color: #4a5568;
            line-height: 1.8;
            min-height: 100px;
        }

        .text-segment {
            display: inline;
            margin-right: 5px;
            padding: 2px 4px;
            border-radius: 3px;
            animation: fadeIn 0.3s;
        }

        .text-segment.partial {
            background: #fef3cd;
            color: #744210;
        }

        .text-segment.final {
            background: #c6f6d5;
            color: #22543d;
        }

        @keyframes fadeIn {
            from { opacity: 0; }
            to { opacity: 1; }
        }

        /* 音频可视化 */
        .audio-visualizer {
            height: 60px;
            margin: 20px 0;
            display: flex;
            align-items: center;
            justify-content: center;
            background: #2d3748;
            border-radius: 10px;
            padding: 10px;
        }

        .audio-bar {
            width: 4px;
            background: #48bb78;
            margin: 0 2px;
            border-radius: 2px;
            transition: height 0.1s;
        }

        /* 设置面板 */
        .settings {
            background: #edf2f7;
            border-radius: 10px;
            padding: 20px;
            margin-bottom: 20px;
        }

        .settings-row {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 15px;
            margin-bottom: 15px;
        }

        .settings-item {
            display: flex;
            flex-direction: column;
        }

        .settings-item label {
            color: #4a5568;
            font-size: 14px;
            margin-bottom: 5px;
        }

        .settings-item input,
        .settings-item select {
            padding: 8px;
            border: 1px solid #cbd5e0;
            border-radius: 5px;
            font-size: 14px;
        }

        /* 日志区域 */
        .log-area {
            background: #2d3748;
            color: #e2e8f0;
            border-radius: 10px;
            padding: 15px;
            max-height: 150px;
            overflow-y: auto;
            font-family: 'Courier New', monospace;
            font-size: 12px;
        }

        .log-entry {
            margin-bottom: 5px;
            padding: 3px 0;
        }

        .log-time {
            color: #718096;
            margin-right: 10px;
        }

        .log-info {
            color: #48bb78;
        }

        .log-error {
            color: #fc8181;
        }

        .log-warning {
            color: #f6ad55;
        }

        /* 统计信息 */
        .stats {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
            gap: 15px;
            margin-top: 20px;
        }

        .stat-card {
            background: white;
            border: 1px solid #e2e8f0;
            border-radius: 8px;
            padding: 15px;
            text-align: center;
        }

        .stat-value {
            font-size: 24px;
            font-weight: bold;
            color: #667eea;
        }

        .stat-label {
            font-size: 12px;
            color: #718096;
            margin-top: 5px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🎤 实时语音识别</h1>
        <p class="subtitle">WebSocket 长连接 + 流式音频传输</p>

        <!-- 连接状态 -->
        <div class="connection-status">
            <div class="status-indicator disconnected" id="statusIndicator"></div>
            <span id="statusText">未连接</span>
        </div>

        <!-- 设置面板 -->
        <div class="settings">
            <div class="settings-row">
                <div class="settings-item">
                    <label for="wsUrl">WebSocket 地址</label>
                    <input type="text" id="wsUrl" value="ws://localhost:48090/ws">
                </div>
                <div class="settings-item">
                    <label for="language">识别语言</label>
                    <select id="language">
                        <option value="zh">中文</option>
                        <option value="en">English</option>
                        <option value="auto">自动检测</option>
                    </select>
                </div>
                <div class="settings-item">
                    <label for="chunkSize">音频块大小 (ms)</label>
                    <input type="number" id="chunkSize" value="1000" min="100" max="5000" step="100">
                </div>
                <div class="settings-item">
                    <label for="streamMode">传输模式</label>
                    <select id="streamMode">
                        <option value="realtime">实时流式</option>
                        <option value="batch">批量发送</option>
                    </select>
                </div>
            </div>
        </div>

        <!-- 控制按钮 -->
        <div class="controls">
            <button id="connectBtn" class="btn-primary">
                <span>🔌</span> 连接
            </button>
            <button id="startStreamBtn" class="btn-success" disabled>
                <span>▶️</span> 开始实时识别
            </button>
            <button id="stopStreamBtn" class="btn-danger" disabled>
                <span>⏹️</span> 停止识别
            </button>
            <button id="pauseBtn" class="btn-primary" disabled>
                <span>⏸️</span> 暂停
            </button>
        </div>

        <!-- 音频可视化 -->
        <div class="audio-visualizer" id="visualizer">
            <!-- 动态生成音频条 -->
        </div>

        <!-- 实时转录区域 -->
        <div class="transcription-area">
            <div class="transcription-header">
                <div class="transcription-title">实时识别结果</div>
                <button class="clear-btn" onclick="clearTranscription()">清空</button>
            </div>
            <div class="realtime-text" id="realtimeText">
                等待开始识别...
            </div>
        </div>

        <!-- 统计信息 -->
        <div class="stats">
            <div class="stat-card">
                <div class="stat-value" id="wordCount">0</div>
                <div class="stat-label">识别字数</div>
            </div>
            <div class="stat-card">
                <div class="stat-value" id="duration">0s</div>
                <div class="stat-label">录音时长</div>
            </div>
            <div class="stat-card">
                <div class="stat-value" id="chunks">0</div>
                <div class="stat-label">音频块数</div>
            </div>
            <div class="stat-card">
                <div class="stat-value" id="latency">0ms</div>
                <div class="stat-label">平均延迟</div>
            </div>
        </div>

        <!-- 日志区域 -->
        <div class="log-area" id="logArea">
            <div class="log-entry">
                <span class="log-time">[00:00:00]</span>
                <span class="log-info">系统就绪</span>
            </div>
        </div>
    </div>

    <script>
        // 全局变量
        let ws = null;
        let mediaRecorder = null;
        let audioStream = null;
        let isRecording = false;
        let isPaused = false;
        let audioChunks = [];
        let recordingStartTime = null;
        let chunkCount = 0;
        let totalWords = 0;
        let latencySum = 0;
        let latencyCount = 0;
        let audioContext = null;
        let analyser = null;
        let visualizerInterval = null;

        // DOM 元素
        const statusIndicator = document.getElementById('statusIndicator');
        const statusText = document.getElementById('statusText');
        const connectBtn = document.getElementById('connectBtn');
        const startStreamBtn = document.getElementById('startStreamBtn');
        const stopStreamBtn = document.getElementById('stopStreamBtn');
        const pauseBtn = document.getElementById('pauseBtn');
        const realtimeText = document.getElementById('realtimeText');
        const logArea = document.getElementById('logArea');
        const visualizer = document.getElementById('visualizer');

        // 初始化音频可视化
        function initVisualizer() {
            // 创建音频条
            visualizer.innerHTML = '';
            for (let i = 0; i < 30; i++) {
                const bar = document.createElement('div');
                bar.className = 'audio-bar';
                bar.style.height = '5px';
                visualizer.appendChild(bar);
            }
        }

        // 更新音频可视化
        function updateVisualizer() {
            if (!analyser) return;

            const bufferLength = analyser.frequencyBinCount;
            const dataArray = new Uint8Array(bufferLength);
            analyser.getByteFrequencyData(dataArray);

            const bars = visualizer.querySelectorAll('.audio-bar');
            const barCount = bars.length;
            const step = Math.floor(bufferLength / barCount);

            for (let i = 0; i < barCount; i++) {
                const value = dataArray[i * step];
                const height = (value / 255) * 40 + 5;
                bars[i].style.height = height + 'px';
            }
        }

        // 添加日志
        function addLog(message, type = 'info') {
            const time = new Date().toLocaleTimeString('zh-CN', { hour12: false });
            const entry = document.createElement('div');
            entry.className = 'log-entry';
            entry.innerHTML = `
                <span class="log-time">[${time}]</span>
                <span class="log-${type}">${message}</span>
            `;
            logArea.appendChild(entry);
            logArea.scrollTop = logArea.scrollHeight;

            // 限制日志条数
            const entries = logArea.querySelectorAll('.log-entry');
            if (entries.length > 100) {
                entries[0].remove();
            }
        }

        // 更新连接状态
        function updateConnectionStatus(status) {
            const statusMap = {
                'connected': { class: 'connected', text: '✅ 已连接', log: '连接成功' },
                'disconnected': { class: 'disconnected', text: '❌ 未连接', log: '连接断开' },
                'connecting': { class: 'connecting', text: '🔄 连接中...', log: '正在连接...' },
                'error': { class: 'disconnected', text: '⚠️ 连接错误', log: '连接错误' }
            };

            const info = statusMap[status];
            statusIndicator.className = `status-indicator ${info.class}`;
            statusText.textContent = info.text;
            addLog(info.log, status === 'error' ? 'error' : 'info');

            // 更新按钮状态
            if (status === 'connected') {
                connectBtn.textContent = '🔌 断开';
                startStreamBtn.disabled = false;
            } else {
                connectBtn.textContent = '🔌 连接';
                startStreamBtn.disabled = true;
                stopStreamBtn.disabled = true;
                pauseBtn.disabled = true;
            }
        }

        // WebSocket 连接管理
        function connectWebSocket() {
            if (ws && ws.readyState === WebSocket.OPEN) {
                ws.close();
                return;
            }

            const wsUrl = document.getElementById('wsUrl').value;
            updateConnectionStatus('connecting');

            ws = new WebSocket(wsUrl);

            // WebSocket 事件处理
            ws.onopen = () => {
                updateConnectionStatus('connected');
                
                // 发送初始化消息
                ws.send(JSON.stringify({
                    type: 'ai-voice-transcription',
                    content: JSON.stringify({
                        type: 'init',
                        config: {
                            language: document.getElementById('language').value,
                            mode: document.getElementById('streamMode').value
                        }
                    })
                }));
            };

            ws.onmessage = (event) => {
                try {
                    const message = JSON.parse(event.data);
                    handleWebSocketMessage(message);
                } catch (e) {
                    addLog('消息解析失败: ' + e.message, 'error');
                }
            };

            ws.onerror = (error) => {
                updateConnectionStatus('error');
                addLog('WebSocket 错误', 'error');
            };

            ws.onclose = () => {
                updateConnectionStatus('disconnected');
                stopRecording();
            };

            // 心跳保持连接
            startHeartbeat();
        }

        // 心跳机制
        let heartbeatInterval = null;
        function startHeartbeat() {
            stopHeartbeat();
            heartbeatInterval = setInterval(() => {
                if (ws && ws.readyState === WebSocket.OPEN) {
                    ws.send(JSON.stringify({
                        type: 'ping',
                        timestamp: Date.now()
                    }));
                }
            }, 30000); // 每30秒发送一次心跳
        }

        function stopHeartbeat() {
            if (heartbeatInterval) {
                clearInterval(heartbeatInterval);
                heartbeatInterval = null;
            }
        }

        // 处理 WebSocket 消息
        function handleWebSocketMessage(message) {
            if (message.type === 'ai-voice-transcription') {
                const content = JSON.parse(message.content);
                
                if (content.text) {
                    // 计算延迟
                    if (content.timestamp) {
                        const latency = Date.now() - content.timestamp;
                        latencySum += latency;
                        latencyCount++;
                        document.getElementById('latency').textContent = 
                            Math.round(latencySum / latencyCount) + 'ms';
                    }

                    // 显示识别结果
                    displayTranscription(content.text, content.finished);
                    
                    // 更新统计
                    const words = content.text.length;
                    totalWords += words;
                    document.getElementById('wordCount').textContent = totalWords;
                }

                if (content.error) {
                    addLog('识别错误: ' + content.error, 'error');
                }
            } else if (message.type === 'pong') {
                // 心跳响应
                const latency = Date.now() - message.timestamp;
                addLog(`心跳延迟: ${latency}ms`, 'info');
            }
        }

        // 显示转录结果
        function displayTranscription(text, isFinal) {
            const span = document.createElement('span');
            span.className = `text-segment ${isFinal ? 'final' : 'partial'}`;
            span.textContent = text + ' ';
            
            if (realtimeText.textContent === '等待开始识别...') {
                realtimeText.innerHTML = '';
            }
            
            realtimeText.appendChild(span);
            
            // 自动滚动
            realtimeText.scrollTop = realtimeText.scrollHeight;
        }

        // 开始实时录音和传输
        async function startRecording() {
            try {
                // 请求麦克风权限
                audioStream = await navigator.mediaDevices.getUserMedia({ 
                    audio: {
                        echoCancellation: true,
                        noiseSuppression: true,
                        sampleRate: 16000
                    }
                });

                addLog('麦克风权限已获取', 'info');

                // 设置音频分析器（用于可视化）
                audioContext = new AudioContext();
                const source = audioContext.createMediaStreamSource(audioStream);
                analyser = audioContext.createAnalyser();
                analyser.fftSize = 256;
                source.connect(analyser);

                // 启动可视化
                visualizerInterval = setInterval(updateVisualizer, 50);

                // 发送开始消息
                ws.send(JSON.stringify({
                    type: 'ai-voice-transcription',
                    content: JSON.stringify({
                        type: 'start',
                        format: 'webm',
                        language: document.getElementById('language').value,
                        timestamp: Date.now()
                    })
                }));

                // 创建 MediaRecorder
                const options = {
                    mimeType: 'audio/webm;codecs=opus'
                };
                mediaRecorder = new MediaRecorder(audioStream, options);

                const chunkSize = parseInt(document.getElementById('chunkSize').value);
                const streamMode = document.getElementById('streamMode').value;

                // 实时模式：定时发送音频块
                if (streamMode === 'realtime') {
                    mediaRecorder.ondataavailable = (event) => {
                        if (event.data.size > 0 && !isPaused) {
                            sendAudioChunk(event.data);
                        }
                    };

                    // 每隔 chunkSize 毫秒发送一次
                    mediaRecorder.start(chunkSize);
                } else {
                    // 批量模式：累积后发送
                    audioChunks = [];
                    mediaRecorder.ondataavailable = (event) => {
                        if (event.data.size > 0) {
                            audioChunks.push(event.data);
                        }
                    };

                    mediaRecorder.onstop = () => {
                        const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
                        sendAudioChunk(audioBlob);
                    };

                    mediaRecorder.start();
                }

                // 更新状态
                isRecording = true;
                recordingStartTime = Date.now();
                startStreamBtn.disabled = true;
                stopStreamBtn.disabled = false;
                pauseBtn.disabled = false;
                startStreamBtn.classList.add('btn-recording');

                addLog('开始录音和实时识别', 'info');
                
                // 更新录音时长
                updateDuration();

            } catch (error) {
                addLog('启动录音失败: ' + error.message, 'error');
                console.error('Recording error:', error);
            }
        }

        // 发送音频块
        async function sendAudioChunk(blob) {
            const reader = new FileReader();
            reader.onloadend = () => {
                const base64data = reader.result;
                
                // 发送音频数据
                ws.send(JSON.stringify({
                    type: 'ai-voice-transcription',
                    content: JSON.stringify({
                        type: 'audio',
                        data: base64data,
                        format: 'webm',
                        language: document.getElementById('language').value,
                        timestamp: Date.now(),
                        chunkIndex: chunkCount++
                    })
                }));

                // 更新统计
                document.getElementById('chunks').textContent = chunkCount;
                addLog(`发送音频块 #${chunkCount}`, 'info');
            };
            reader.readAsDataURL(blob);
        }

        // 停止录音
        function stopRecording() {
            if (mediaRecorder && isRecording) {
                mediaRecorder.stop();
                
                // 发送停止消息
                ws.send(JSON.stringify({
                    type: 'ai-voice-transcription',
                    content: JSON.stringify({
                        type: 'stop',
                        timestamp: Date.now()
                    })
                }));

                // 停止音频流
                if (audioStream) {
                    audioStream.getTracks().forEach(track => track.stop());
                }

                // 停止可视化
                if (visualizerInterval) {
                    clearInterval(visualizerInterval);
                    visualizerInterval = null;
                }

                // 重置可视化
                initVisualizer();

                // 更新状态
                isRecording = false;
                isPaused = false;
                startStreamBtn.disabled = false;
                stopStreamBtn.disabled = true;
                pauseBtn.disabled = true;
                startStreamBtn.classList.remove('btn-recording');
                pauseBtn.textContent = '⏸️ 暂停';

                addLog('停止录音', 'info');
            }
        }

        // 暂停/恢复
        function togglePause() {
            if (isPaused) {
                isPaused = false;
                pauseBtn.textContent = '⏸️ 暂停';
                if (mediaRecorder && mediaRecorder.state === 'paused') {
                    mediaRecorder.resume();
                }
                addLog('恢复录音', 'info');
            } else {
                isPaused = true;
                pauseBtn.textContent = '▶️ 继续';
                if (mediaRecorder && mediaRecorder.state === 'recording') {
                    mediaRecorder.pause();
                }
                addLog('暂停录音', 'warning');
            }
        }

        // 更新录音时长
        function updateDuration() {
            if (isRecording && recordingStartTime) {
                const duration = Math.floor((Date.now() - recordingStartTime) / 1000);
                document.getElementById('duration').textContent = duration + 's';
                
                if (!isPaused) {
                    setTimeout(updateDuration, 1000);
                }
            }
        }

        // 清空转录内容
        function clearTranscription() {
            realtimeText.innerHTML = '等待开始识别...';
            totalWords = 0;
            document.getElementById('wordCount').textContent = '0';
            addLog('清空识别结果', 'info');
        }

        // 重置统计
        function resetStats() {
            chunkCount = 0;
            totalWords = 0;
            latencySum = 0;
            latencyCount = 0;
            document.getElementById('wordCount').textContent = '0';
            document.getElementById('duration').textContent = '0s';
            document.getElementById('chunks').textContent = '0';
            document.getElementById('latency').textContent = '0ms';
        }

        // 绑定事件
        connectBtn.addEventListener('click', connectWebSocket);
        startStreamBtn.addEventListener('click', () => {
            resetStats();
            startRecording();
        });
        stopStreamBtn.addEventListener('click', stopRecording);
        pauseBtn.addEventListener('click', togglePause);

        // 页面卸载时清理
        window.addEventListener('beforeunload', () => {
            stopRecording();
            stopHeartbeat();
            if (ws) {
                ws.close();
            }
        });

        // 初始化
        initVisualizer();
        addLog('页面加载完成，请点击"连接"按钮开始', 'info');
    </script>
</body>
</html>
